package com.imooc.bilibili.video.impl;


import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.github.tobato.fastdfs.service.FastFileStorageClient;
import com.imooc.bilibili.domain.file.File;
import com.imooc.bilibili.domain.user.UserCoin;
import com.imooc.bilibili.domain.user.UserInfo;
import com.imooc.bilibili.domain.video.*;
import com.imooc.bilibili.exception.ConditionException;
import com.imooc.bilibili.file.IFileService;
import com.imooc.bilibili.mapper.user.UserCoinMapper;
import com.imooc.bilibili.mapper.video.*;
import com.imooc.bilibili.user.service.UserInfoService;
import com.imooc.bilibili.util.FastDFSUtil;
import com.imooc.bilibili.util.ImageUtil;
import com.imooc.bilibili.util.IpUtil;
import com.imooc.bilibili.video.IVideoBinaryPictureService;
import com.imooc.bilibili.video.IVideoService;
import com.imooc.bilibili.video.IVideoTagService;
import eu.bitwalker.useragentutils.UserAgent;
import org.apache.mahout.cf.taste.common.TasteException;
import org.apache.mahout.cf.taste.impl.common.FastByIDMap;
import org.apache.mahout.cf.taste.impl.model.GenericDataModel;
import org.apache.mahout.cf.taste.impl.model.GenericPreference;
import org.apache.mahout.cf.taste.impl.model.GenericUserPreferenceArray;
import org.apache.mahout.cf.taste.impl.neighborhood.NearestNUserNeighborhood;
import org.apache.mahout.cf.taste.impl.recommender.GenericUserBasedRecommender;
import org.apache.mahout.cf.taste.impl.similarity.UncenteredCosineSimilarity;
import org.apache.mahout.cf.taste.model.DataModel;
import org.apache.mahout.cf.taste.model.PreferenceArray;
import org.apache.mahout.cf.taste.recommender.RecommendedItem;
import org.bytedeco.javacv.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.stream.Collectors;

/**
 * <p>
 * 视频投稿记录表 服务实现类
 * </p>
 *
 * @author gcq
 * @since 2022-06-04
 */
@Service
public class VideoServiceImpl extends ServiceImpl<VideoMapper, Video> implements IVideoService {

    @Autowired
    private VideoMapper videoMapper;

    @Autowired
    private IVideoTagService videoTagService;

    @Autowired
    private FastDFSUtil fastDFSUtil;

    @Autowired
    private FastFileStorageClient fastFileStorageClient;

    @Autowired
    private VideoLikeMapper videoLikeMapper;

    @Autowired
    private VideoCollectionMapper videoCollectionMapper;

    @Autowired
    private UserCoinMapper userCoinMapper;

    @Autowired
    private VideoCoinMapper videoCoinMapper;

    @Autowired
    private VideoCommentMapper videoCommentMapper;
    
    @Autowired
    private UserInfoService userInfoService;

    @Autowired
    private VideoViewMapper videoViewMapper;
    
    @Autowired
    IFileService fileService;

    @Autowired
    private ImageUtil imageUtil;

    @Autowired
    private IVideoBinaryPictureService videoBinaryPictureService;

    private static final int FRAME_NO = 256;

    @Transactional
    @Override
    public void addVideos(Video video) {
        video.setCreateTime(new Date());
        videoMapper.insert(video);
        Long videoId = video.getId();
        List<VideoTag> tagList = video.getVideoTagList();
        tagList.forEach(item -> {
            item.setCreateTime(new Date());
            item.setVideoId(videoId);
        });
        videoTagService.saveBatch(tagList);
    }

    @Override
    public Page<Video> pageListVideos(Integer size, Integer no, String area) {
        Page<Video> page = new Page<>(size, no);
        return videoMapper.selectPage(page, new QueryWrapper<Video>().eq("area", area));
    }

    @Override
    public void viewVideoOnlineBySlices(HttpServletRequest request, HttpServletResponse response, String url) throws Exception {
        fastDFSUtil.viewVideoOnlineBySlices(request, response, url);
    }

    @Override
    public void addVideoLikes(Long userId, Long videoId) {
        Video video = videoMapper.selectById(videoId);
        if (video == null) {
            throw new ConditionException("非法视频!");
        }
        VideoLike videoLike = videoLikeMapper.selectOne(new QueryWrapper<VideoLike>().eq("userId", userId).eq("videoId", videoId));
        if (videoLike == null) {
            throw new ConditionException("已经赞过");
        }
        videoLike = new VideoLike();
        videoLike.setVideoId(videoId);
        videoLike.setCreateTime(new Date());
        videoLike.setUserId(userId);
        videoLikeMapper.insert(videoLike);
    }

    @Override
    public void deleteVideoLikes(Long userId, Long videoId) {
        videoLikeMapper.delete(new QueryWrapper<VideoLike>().eq("userId", userId).eq("videoId", videoId));
    }

    @Override
    public Map<String, Object> getVideoLikes(Long videoId, Long userId) {
        Long count = videoLikeMapper.selectCount(new QueryWrapper<VideoLike>().eq("videoId", videoId));
        VideoLike videoLike = videoLikeMapper.selectOne(new QueryWrapper<VideoLike>().eq("userId", userId).eq("videoId", videoId));
        boolean like = videoLike != null;
        Map<String, Object> result = new HashMap<>();
        result.put("count", count);
        result.put("like", like);
        return result;
    }

    @Transactional
    @Override
    public void addVideoCollection(VideoCollection videoCollection) {
        Video video = videoMapper.selectById(videoCollection.getVideoId());
        if (video == null) {
            throw new ConditionException("非法视频");
        }
        // 删除原有收藏
        videoCollectionMapper.delete(new QueryWrapper<VideoCollection>().
                eq("userId", videoCollection.getUserId()).
                eq("videoId", videoCollection.getVideoId()));
        videoCollection.setCreateTime(new Date());
        // 新增收藏
        videoCollectionMapper.insert(videoCollection);
    }

    @Override
    public void deleteVideoCollection(Long userId, Long videoId) {
        videoCollectionMapper.delete(new QueryWrapper<VideoCollection>().
                eq("userId", userId).
                eq("videoId", videoId));
    }

    @Override
    public Map<String, Object> getVideoCollections(Long userId, Long videoId) {
        Long count = videoCollectionMapper.selectCount(new QueryWrapper<VideoCollection>().
                eq("videoId", videoId));
        VideoCollection videoCollection = videoCollectionMapper.selectOne(new QueryWrapper<VideoCollection>().
                eq("userId", userId).
                eq("videoId", videoId));
        boolean like = videoCollection != null;
        Map<String, Object> result = new HashMap<>();
        result.put("count", count);
        result.put("like", like);
        return result;
    }

    @Transactional
    @Override
    public void addVideoCoins(VideoCoin videoCoin, Long userId) {
        Integer amount = videoCoin.getAmount();
        Video video = videoMapper.selectById(videoCoin.getVideoId());
        if (video == null) {
            throw new ConditionException("非法视频!");
        }
        // 查询当前登录用户是否有足够硬币
        UserCoin userCoin = userCoinMapper.selectOne(new QueryWrapper<UserCoin>().eq("userId", userId));
        Long userCoinAmount = userCoin.getAmount();
        userCoinAmount = userCoinAmount == null ? 0 : userCoinAmount;
        if (amount > userCoinAmount) {
            throw new ConditionException("硬币数量不足!");
        }
        // 查询当前用户对该视频投了多少币
        VideoCoin dbVideoCoin = videoCoinMapper.selectOne(new QueryWrapper<VideoCoin>().
                eq("userId", userId).
                eq("videoId", videoCoin.getVideoId()));
        if (dbVideoCoin == null) {
            videoCoin.setCreateTime(new Date());
            videoCoin.setUserId(userId);
            videoCoinMapper.insert(videoCoin);
        } else {
            Integer dbAmount = dbVideoCoin.getAmount();
            dbAmount += amount;
            videoCoin.setAmount(dbAmount);
            videoCoin.setUserId(userId);
            videoCoin.setUpdateTime(new Date());
            videoCoinMapper.update(videoCoin, new QueryWrapper<VideoCoin>().
                    eq("userId", userId).
                    eq("videoId", videoCoin.getVideoId()));
        }
        // 更新当前用户硬币总数量
        userCoinMapper.update(null, new UpdateWrapper<UserCoin>().
                set("amount", (userCoinAmount - amount)).
                set("updateTime", new Date()).
                eq("userId", userId));
    }

    @Override
    public Map<String, Object> getVideoCoins(Long videoId, Long userId) {
        Long amount = videoCoinMapper.selectAmountSumByVideoId(videoId);
        VideoCoin videoCoin = videoCoinMapper.selectOne(new QueryWrapper<VideoCoin>().
                eq("userId", userId).
                eq("videoId", videoId));
        boolean like = videoCoin != null;
        Map<String, Object> result = new HashMap<>();
        result.put("count", amount);
        result.put("like", like);
        return result;
    }

    @Override
    public void addVideoComment(VideoComment videoComment) {
        Long videoId = videoComment.getVideoId();
        Video video = videoMapper.selectById(videoId);
        if (video == null) {
            throw new ConditionException("非法视频");
        }
        videoComment.setCreateTime(new Date());
        videoCommentMapper.insert(videoComment);
    }

    /**
     * 目前只支持两级评论
     *
     * @param size
     * @param no
     * @param videoId
     * @return
     */
    @Override
    public Page<VideoComment> pageListVideoComments(Integer size, Integer no, Long videoId) {
        Video video = videoMapper.selectById(videoId);
        if(video == null) {
            throw new ConditionException("非法视频");
        }
        Page<VideoComment> page = new Page<>(size, no);
        Page<VideoComment> pageList = videoCommentMapper.selectPage(page, new QueryWrapper<VideoComment>().
                eq("videoId", video).
                isNull("rootId")
                .orderByDesc("id"));
        List<VideoComment> videoCommentList = page.getRecords();

        if(pageList.getTotal() > 0) {
            // 批量查询二级评论
            // 当前视频所有一级评论Id
            List<Long> parentIdList = videoCommentList.stream().map(VideoComment::getId).collect(Collectors.toList());
            // 根节点评论信息
            List<VideoComment> childCommentList = videoCommentMapper.batchGetVideoCommentByRootIds(parentIdList);
            // 一级评论所有用户Id
            Set<Long> userIdList = videoCommentList.stream().map(VideoComment::getUserId).collect(Collectors.toSet());
            // 二级评论所有回复用户Id
            Set<Long> replyUserIdList = childCommentList.stream().map(VideoComment::getReplyUserId).collect(Collectors.toSet());
            userIdList.addAll(replyUserIdList);

            // 评论用户信息
            List<UserInfo> userInfoList = userInfoService.getBaseMapper().selectBatchIds(userIdList);
            // 转换成map方便提取
            Map<Long, UserInfo> userInfoMap = userInfoList.stream().collect(Collectors.toMap(UserInfo::getUserId, userInfo -> userInfo));
            videoCommentList.forEach(comment -> {
                Long id = comment.getId();
                List<VideoComment> childList = new ArrayList<>();
                // 当前一级评论下面的多个二级评论
                childCommentList.forEach(child -> {
                    if(id.equals(child.getRootId())) {
                        child.setUserInfo(userInfoMap.get(child.getUserId()));
                        child.setReplyUserInfo(userInfoMap.get(child.getReplyUserId()));
                        childList.add(child);
                    }
                });
                // 当前评论的二级评论信息
                comment.setVideoCommentList(childList);
                comment.setUserInfo(userInfoMap.get(comment.getUserId()));
            });
        }
        return pageList;
    }

    @Override
    public Map<String, Object> getVideoDetails(Long videoId) {
        Video video = videoMapper.selectById(videoId);
        Long userId = video.getUserId();
        UserInfo userInfo = userInfoService.getOne(new QueryWrapper<UserInfo>().eq("userId", userId));
        Map<String, Object> result = new HashMap<>();
        result.put("video", video);
        result.put("userInfo", userInfo);
        return result;
    }

    /**
     * 添加视频观看记录
     * 两种情况
     * 1、用户已登录，观看视频，则记录ip和clientId
     * 2、用户未登录，观看视频，不记录ip和clientId
     * 一个用户一天只能观看一次视频
     * 在保存前，需查询当前用户，是否观看过该视频，如已观看过该视频，不进行添加
     * @param videoView
     * @param request
     */
    @Override
    public void addVideoView(VideoView videoView, HttpServletRequest request) {
        Long videoId = videoView.getVideoId();
        Long userId = videoView.getUserId();
        // 生成clientId
        String agent = request.getHeader("User-Agent");
        UserAgent userAgent = UserAgent.parseUserAgentString(agent);
        String clientId = String.valueOf(userAgent.getId());
        String ip = IpUtil.getIP(request);
        Map<String, Object> params = new HashMap<>();
        if(userId != null) {
            params.put("userId", userId);
        } else {
            params.put("ip", ip);
            params.put("clientId", clientId);
        }
        Date now = new Date();
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        params.put("today", sdf.format(now));
        params.put("videoId", videoId);
        // 添加观看记录
        VideoView dbVideoView = videoMapper.getVideoView(params);
        if(dbVideoView == null) {
            videoView.setCreateTime(new Date());
            videoView.setClientId(clientId);
            videoView.setIp(ip);
            videoViewMapper.insert(videoView);
        }
    }

    @Override
    public Long getVideoViewCounts(Long videoId) {
        return videoViewMapper.selectCount(new QueryWrapper<VideoView>().eq("videoId", videoId));
    }

    @Override
    public List<Video> recommend(Long userId) throws TasteException {
        // 获取用户偏好
        List<VideoOperation> videoOperationList = videoMapper.getAllUserPreference();
        // 创建好数据模型
        DataModel dataModel = this.createDataModel(videoOperationList);
        // 获取用户相似程度
        UncenteredCosineSimilarity similarity = new UncenteredCosineSimilarity(dataModel);
        System.out.println(similarity.userSimilarity(18, 19));
        // 用户邻居
        NearestNUserNeighborhood nearestNUserNeighborhood = new NearestNUserNeighborhood(2, similarity, dataModel);
        long[] ar = nearestNUserNeighborhood.getUserNeighborhood(userId);
        // 构建推荐器
        GenericUserBasedRecommender basedRecommender = new GenericUserBasedRecommender(dataModel, nearestNUserNeighborhood, similarity);
        // 推荐商品
        List<RecommendedItem> recommendItems = basedRecommender.recommend(userId, 2);
        List<Long> itemIds = recommendItems.stream().map(RecommendedItem::getItemID).collect(Collectors.toList());
        if(itemIds.size() == 0){
            return Collections.emptyList();
        }
        return videoMapper.selectBatchIds(itemIds);
    }

    @Override
    public List<VideoBinaryPicture> convertVideoToImage(Long videoId, String fileMd5) throws Exception {
        File dbFile = fileService.getOne(new QueryWrapper<File>().eq("md5", fileMd5));
        String filePath = "D:\\workspace\\gitee\\idea_projects\\imooc-bilibili" + videoId + "." + dbFile.getType();
        fastDFSUtil.downloadFile(dbFile.getUrl(), filePath);
        FFmpegFrameGrabber fFmpegFrameGrabber = FFmpegFrameGrabber.createDefault(filePath);
        fFmpegFrameGrabber.start();
        int ffLength = fFmpegFrameGrabber.getLengthInFrames();
        Frame frame;
        Java2DFrameConverter converter = new Java2DFrameConverter();
        int count = 1;
        List<VideoBinaryPicture> pictures = new ArrayList<>();
        for (int i = 0; i < ffLength; i++) {
            long timestamp = fFmpegFrameGrabber.getTimestamp();
            frame = fFmpegFrameGrabber.grabImage();
            if(count == i) {
                if(frame == null) {
                    throw new ConditionException("无效帧");
                }
                BufferedImage bufferedImage = converter.getBufferedImage(frame);
                ByteArrayOutputStream os = new ByteArrayOutputStream();
                ImageIO.write(bufferedImage, "png", os);
                ByteArrayInputStream inputStream = new ByteArrayInputStream(os.toByteArray());
                // 输出黑白剪影
                java.io.File outputFile = java.io.File.createTempFile("convert-" + videoId + "-", ".png");
                BufferedImage binaryImg = imageUtil.getBodyOutline(bufferedImage, inputStream);
                ImageIO.write(binaryImg, "png", outputFile);
                // 有的浏览器或网站需要把图片白色部分转换成透明色，使用以下方法可实现
                imageUtil.transferAlpha(outputFile, outputFile);
                // 上传视频剪影文件
                String imgUrl = fastDFSUtil.uploadCommonFile(outputFile, "png");
                VideoBinaryPicture binaryPicture = new VideoBinaryPicture();
                binaryPicture.setFrameNo(i);
                binaryPicture.setUrl(imgUrl);
                binaryPicture.setVideoId(videoId);
                binaryPicture.setVideoTimestamp(timestamp);
                pictures.add(binaryPicture);
                count += FRAME_NO;
                // 删除临时文件
                outputFile.delete();
            }
        }
        java.io.File tempFile = new java.io.File(filePath);
        tempFile.delete();
        // 批量添加视频剪影文件
        videoBinaryPictureService.saveBatch(pictures);
        return pictures;
    }

    /**
     * 生成数据模型 基于 用户偏好
     * @param userPreferenceList
     * @return
     */
    private DataModel createDataModel(List<VideoOperation> userPreferenceList) {
        FastByIDMap<PreferenceArray> fastByIdMap = new FastByIDMap<>();
        Map<Long, List<VideoOperation>> map = userPreferenceList.stream().collect(Collectors.groupingBy(VideoOperation::getUserId));
        Collection<List<VideoOperation>> list = map.values();
        for(List<VideoOperation> userPreferences : list){
            GenericPreference[] array = new GenericPreference[userPreferences.size()];
            for(int i = 0; i < userPreferences.size(); i++){
                VideoOperation userPreference = userPreferences.get(i);
                GenericPreference item = new GenericPreference(userPreference.getUserId(), userPreference.getVideoId(), userPreference.getValue());
                array[i] = item;
            }
            fastByIdMap.put(array[0].getUserID(), new GenericUserPreferenceArray(Arrays.asList(array)));
        }
        return new GenericDataModel(fastByIdMap);
    }

}
